1 /** 2 Copyright: Copyright (c) 2021, Joakim Brännström. All rights reserved. 3 License: MPL-2 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 This Source Code Form is subject to the terms of the Mozilla Public License, 7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain 8 one at http://mozilla.org/MPL/2.0/. 9 10 Code copied from dextool 11 */ 12 module code_checker.database.schema; 13 14 import logger = std.experimental.logger; 15 import std.array : array, empty; 16 import std.datetime : SysTime, dur, Clock; 17 import std.exception : collectException; 18 import std.format : format; 19 20 import d2sqlite3 : SqlDatabase = Database; 21 import miniorm : Miniorm, TableName, buildSchema, ColumnParam, TableForeignKey, TableConstraint, 22 TablePrimaryKey, KeyRef, KeyParam, ColumnName, delete_, insert, select, spinSql; 23 import my.path : AbsolutePath; 24 25 /** Initialize or open an existing database. 26 * 27 * Params: 28 * p = path where to initialize a new database or open an existing 29 * 30 * Returns: an open sqlite3 database object. 31 */ 32 Miniorm initializeDB(AbsolutePath p) @trusted 33 in { 34 assert(p.length != 0); 35 } 36 do { 37 import std.file : exists; 38 import my.file : followSymlink; 39 import my.optional; 40 import my.path : Path; 41 import d2sqlite3 : SQLITE_OPEN_CREATE, SQLITE_OPEN_READWRITE; 42 43 static void setPragmas(ref SqlDatabase db) { 44 // dfmt off 45 auto pragmas = [ 46 // required for foreign keys with cascade to work 47 "PRAGMA foreign_keys=ON;", 48 ]; 49 // dfmt on 50 51 foreach (p; pragmas) { 52 db.run(p); 53 } 54 } 55 56 const isOldDb = exists(followSymlink(Path(p)).orElse(Path(p)).toString); 57 SqlDatabase sqliteDb; 58 scope (success) 59 setPragmas(sqliteDb); 60 61 logger.trace("Opening database ", p); 62 try { 63 sqliteDb = SqlDatabase(p, SQLITE_OPEN_READWRITE); 64 } catch (Exception e) { 65 logger.trace(e.msg); 66 logger.trace("Initializing a new sqlite3 database"); 67 sqliteDb = SqlDatabase(p, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE); 68 } 69 70 auto db = Miniorm(sqliteDb); 71 72 auto tbl = makeUpgradeTable; 73 const longTimeout = 10.dur!"minutes"; 74 try { 75 if (isOldDb 76 && spinSql!(() => getSchemaVersion(db))(10.dur!"seconds") >= tbl 77 .latestSchemaVersion) 78 return db; 79 } catch (Exception e) { 80 logger.info("The database is probably locked. Will keep trying to open for ", longTimeout); 81 } 82 if (isOldDb && spinSql!(() => getSchemaVersion(db))(longTimeout) >= tbl.latestSchemaVersion) 83 return db; 84 85 // TODO: remove all key off in upgrade schemas. 86 const giveUpAfter = Clock.currTime + longTimeout; 87 bool failed = true; 88 while (failed && Clock.currTime < giveUpAfter) { 89 try { 90 auto trans = db.transaction; 91 db.run("PRAGMA foreign_keys=OFF;"); 92 upgrade(db, tbl); 93 trans.commit; 94 failed = false; 95 } catch (Exception e) { 96 logger.trace(e.msg); 97 } 98 } 99 100 if (failed) { 101 logger.error("Unable to upgrade the database to the latest schema"); 102 throw new Exception(null); 103 } 104 105 return db; 106 } 107 108 struct UpgradeTable { 109 alias UpgradeFunc = void function(ref Miniorm db); 110 UpgradeFunc[long] tbl; 111 alias tbl this; 112 113 immutable long latestSchemaVersion; 114 } 115 116 /** Inspects a module for functions starting with upgradeV to create a table of 117 * functions that can be used to upgrade a database. 118 */ 119 UpgradeTable makeUpgradeTable() { 120 import std.algorithm : sort, startsWith; 121 import std.conv : to; 122 import std.typecons : Tuple; 123 124 immutable prefix = "upgradeV"; 125 126 alias Module = code_checker.database.schema; 127 128 // the second parameter is the database version to upgrade FROM. 129 alias UpgradeFx = Tuple!(UpgradeTable.UpgradeFunc, long); 130 131 UpgradeFx[] upgradeFx; 132 long last_from; 133 134 static foreach (member; __traits(allMembers, Module)) { 135 static if (member.startsWith(prefix)) 136 upgradeFx ~= UpgradeFx(&__traits(getMember, Module, member), 137 member[prefix.length .. $].to!long); 138 } 139 140 typeof(UpgradeTable.tbl) tbl; 141 foreach (fn; upgradeFx.sort!((a, b) => a[1] < b[1])) { 142 last_from = fn[1]; 143 tbl[last_from] = fn[0]; 144 } 145 146 return UpgradeTable(tbl, last_from + 1); 147 } 148 149 void updateSchemaVersion(ref Miniorm db, long ver) nothrow { 150 try { 151 db.run(delete_!VersionTbl); 152 db.run(insert!VersionTbl.insert, VersionTbl(ver)); 153 } catch (Exception e) { 154 logger.error(e.msg).collectException; 155 } 156 } 157 158 long getSchemaVersion(ref Miniorm db) { 159 auto v = db.run(select!VersionTbl); 160 return v.empty ? 0 : v.front.version_; 161 } 162 163 void upgrade(ref Miniorm db, UpgradeTable tbl) { 164 import d2sqlite3; 165 166 immutable maxIndex = 30; 167 168 alias upgradeFunc = void function(ref Miniorm db); 169 170 bool hasUpdated; 171 172 bool running = true; 173 while (running) { 174 const version_ = () { 175 // first time the version table do not exist thus fail. 176 try { 177 return getSchemaVersion(db); 178 } catch (Exception e) { 179 } 180 return 0; 181 }(); 182 183 if (version_ >= tbl.latestSchemaVersion) { 184 running = false; 185 break; 186 } 187 188 logger.infof("Upgrading database from %s", version_).collectException; 189 190 if (!hasUpdated) 191 try { 192 // only do this once and always before any changes to the database. 193 foreach (i; 0 .. maxIndex) { 194 db.run(format!"DROP INDEX IF EXISTS i%s"(i)); 195 } 196 } catch (Exception e) { 197 logger.warning(e.msg).collectException; 198 logger.warning("Unable to drop database indexes").collectException; 199 } 200 201 if (auto f = version_ in tbl) { 202 try { 203 hasUpdated = true; 204 205 (*f)(db); 206 if (version_ != 0) 207 updateSchemaVersion(db, version_ + 1); 208 } catch (Exception e) { 209 logger.trace(e).collectException; 210 logger.error(e.msg).collectException; 211 logger.warningf("Unable to upgrade a database of version %s", 212 version_).collectException; 213 logger.warning("This might impact the functionality. It is unwise to continue") 214 .collectException; 215 throw e; 216 } 217 } else { 218 logger.info("Upgrade successful").collectException; 219 running = false; 220 } 221 } 222 } 223 224 immutable schemaVersionTable = "schema_version"; 225 @TableName(schemaVersionTable) 226 struct VersionTbl { 227 @ColumnName("version") 228 long version_; 229 } 230 231 immutable filesTable = "files"; 232 @TableName(filesTable) 233 @TableConstraint("unique_ UNIQUE (path)") 234 struct FilesTbl { 235 long id; 236 string path; 237 long checksum; 238 239 /// True if the file is a root. 240 bool root; 241 242 @ColumnName("time_stamp") 243 SysTime timeStamp; 244 } 245 246 immutable depFileTable = "dependency_file"; 247 /** Files that roots are dependent on. They do not need to contain mutants. 248 */ 249 @TableName(depFileTable) 250 @TableConstraint("unique_ UNIQUE (file)") 251 struct DependencyFileTable { 252 long id; 253 string file; 254 long checksum; 255 256 @ColumnName("time_stamp") 257 SysTime timeStamp; 258 } 259 260 immutable depRootTable = "rel_dependency_root"; 261 @TableName(depRootTable) 262 @TableForeignKey("dep_id", KeyRef("dependency_file(id)"), KeyParam("ON DELETE CASCADE")) 263 @TableForeignKey("file_id", KeyRef("files(id)"), KeyParam("ON DELETE CASCADE")) 264 @TableConstraint("unique_ UNIQUE (dep_id, file_id)") 265 struct DependencyRootTable { 266 @ColumnName("dep_id") 267 long depFileId; 268 269 @ColumnName("file_id") 270 long fileId; 271 } 272 273 immutable compileDbTrack = "compile_db_track"; 274 @TableName(compileDbTrack) 275 @TableConstraint("unique_ UNIQUE (path)") 276 struct CompileDbTrackTable { 277 long id; 278 279 string path; 280 281 @ColumnName("time_stamp") 282 SysTime timeStamp; 283 284 long checksum; 285 } 286 287 /** If the database start it version 0, not initialized, then initialize to the 288 * latest schema version. 289 */ 290 void upgradeV0(ref Miniorm db) { 291 auto tbl = makeUpgradeTable; 292 293 db.run(buildSchema!(VersionTbl, FilesTbl, DependencyFileTable, 294 DependencyRootTable, CompileDbTrackTable)); 295 updateSchemaVersion(db, tbl.latestSchemaVersion); 296 } 297 298 void upgradeV1(ref Miniorm db) { 299 import miniorm : toSqliteDateTime; 300 301 immutable newTbl = "new_" ~ filesTable; 302 db.run(buildSchema!FilesTbl("new_")); 303 auto stmt = db.prepare(format( 304 "INSERT INTO %s (id,path,checksum,root,time_stamp) SELECT id,path,checksum,root,:ts FROM %s", 305 newTbl, filesTable)); 306 stmt.get.bind(":ts", Clock.currTime.toSqliteDateTime); 307 stmt.get.execute; 308 309 replaceTbl(db, newTbl, filesTable); 310 db.run("DROP TABLE " ~ depFileTable); 311 db.run("DELETE FROM " ~ depRootTable); 312 db.run(buildSchema!(DependencyFileTable, FilesTbl)); 313 } 314 315 void upgradeV2(ref Miniorm db) { 316 @TableName(compileDbTrack) 317 @TableConstraint("unique_ UNIQUE (path)") 318 struct CompileDbTrackTable { 319 long id; 320 321 string path; 322 323 long size; 324 325 @ColumnName("time_stamp") 326 SysTime timeStamp; 327 328 } 329 330 db.run(buildSchema!CompileDbTrackTable); 331 } 332 333 void upgradeV3(ref Miniorm db) { 334 db.run("DROP TABLE " ~ compileDbTrack); 335 db.run(buildSchema!CompileDbTrackTable); 336 } 337 338 void replaceTbl(ref Miniorm db, string src, string dst) { 339 db.run("DROP TABLE " ~ dst); 340 db.run(format("ALTER TABLE %s RENAME TO %s", src, dst)); 341 }